#include "mediastreamer2/msfilter.h"
#include "mediastreamer2/msvideo.h"

#include "libyuv/convert_argb.h"
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <native_window/external_window.h>

#undef GL_RGBA
#define GL_RGBA GL_BGRA_EXT
#define GL_BGRA GL_BGRA_EXT

#ifdef __cplusplus
extern "C" {
#endif

enum { ATTRIB_VERTEX, ATTRIB_TEXTUREPOSITON, NUM_ATTRIBUTES };

static const GLchar *vertSrc = " \
attribute vec4 position; \
attribute vec4 inTexCoord; \
varying vec2 texCoord; \
void main() \
{ \
    gl_Position = position; \
    texCoord = inTexCoord.xy; \
} \
";

static const GLchar *fragSrc = " \
varying highp vec2 texCoord; \
uniform sampler2D videoFrame; \
void main() \
{ \
    gl_FragColor = texture2D(videoFrame, texCoord); \
} \
";

struct gl_buffers {
	GLuint frameBuf;
	GLuint rendBuf;
	GLuint rendTex;
	GLuint directProg;

	int rendBufW;
	int rendBufH;
	int direct;
};

struct ohosgl_stream {

	void *user_data; /**< Application data  */

	unsigned ts_inc;

	EGLDisplay display;
	EGLSurface surface;
	EGLContext context;
	OHNativeWindow *window = NULL;
	gl_buffers *gl_buf = NULL;
	int32_t err_rend = 8;
};

static void pjmedia_vid_dev_opengl_destroy_buffers(gl_buffers *glb)
{
    if (glb->frameBuf) {
        glDeleteFramebuffers(1, &glb->frameBuf);
        glb->frameBuf = 0;
    }
    
    if (glb->rendBuf) {
        glDeleteRenderbuffers(1, &glb->rendBuf);
        glb->rendBuf = 0;
    }
    
    if (glb->rendTex) {
        glDeleteTextures(1, &glb->rendTex);
        glb->rendTex = 0;
    }
    
    if (glb->directProg) {
        glDeleteProgram(glb->directProg);
        glb->directProg = 0;
    }
}

static void deinit_opengl(void *data) {
	struct ohosgl_stream *stream = (struct ohosgl_stream *)data;

	if (stream->gl_buf) {
	    pjmedia_vid_dev_opengl_destroy_buffers(stream->gl_buf);
	    stream->gl_buf = NULL;
	}

	if (stream->display != EGL_NO_DISPLAY) {
		eglMakeCurrent(stream->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
		if (stream->context != EGL_NO_CONTEXT) eglDestroyContext(stream->display, stream->context);
		if (stream->surface != EGL_NO_SURFACE) eglDestroySurface(stream->display, stream->surface);
		eglTerminate(stream->display);
	}

	stream->display = EGL_NO_DISPLAY;
	stream->surface = EGL_NO_SURFACE;
	stream->context = EGL_NO_CONTEXT;
}

GLint compile_shader(GLenum target, GLsizei count, const GLchar **sources, GLuint *shader) {
	GLint status;

	*shader = glCreateShader(target);
	glShaderSource(*shader, count, sources, NULL);
	glCompileShader(*shader);

	glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);

	return status;
}

/* Create program, compile shader, link program, bind attributes */
GLint create_program(const GLchar *vertSource,
                     const GLchar *fragSource,
                     GLsizei attribNameCnt,
                     const GLchar **attribNames,
                     const GLint *attribLocations,
                     GLuint *program) {
	GLuint vertShader = 0, fragShader = 0, prog = 0, i;
	GLint status;

	/* Create shader program */
	prog = glCreateProgram();
	*program = prog;

	/* Create and compile vertex shader */
	status = compile_shader(GL_VERTEX_SHADER, 1, &vertSource, &vertShader);
	if (status == 0) {
		ms_message("Unable to compile vertex shader");
		return status;
	}

	/* Create and compile fragment shader */
	status = compile_shader(GL_FRAGMENT_SHADER, 1, &fragSource, &fragShader);
	if (status == 0) {
		ms_message("Unable to compile fragment shader");
		return status;
	}

	/* Attach vertex shader to program */
	glAttachShader(prog, vertShader);

	/* Attach fragment shader to program */
	glAttachShader(prog, fragShader);

	/* Bind attribute locations prior to linking */
	for (i = 0; i < attribNameCnt; i++) {
		glBindAttribLocation(prog, attribLocations[i], attribNames[i]);
	}

	/* Link program */
	glLinkProgram(prog);
	glGetProgramiv(prog, GL_LINK_STATUS, &status);
	if (status == 0) {
		ms_message("Unable to link program");
		return status;
	}

	/* Release vertex and fragment shaders */
	if (vertShader) glDeleteShader(vertShader);
	if (fragShader) glDeleteShader(fragShader);

	return status;
}

static void init11_opengl(struct ohosgl_stream *strm) {
	const EGLint attr[] = {EGL_RENDERABLE_TYPE,
	                       EGL_OPENGL_ES2_BIT,
	                       EGL_SURFACE_TYPE,
	                       EGL_WINDOW_BIT,
	                       EGL_BLUE_SIZE,
	                       8,
	                       EGL_GREEN_SIZE,
	                       8,
	                       EGL_RED_SIZE,
	                       8,
	                       EGL_ALPHA_SIZE,
	                       8,
	                       EGL_DEPTH_SIZE,
	                       8,
	                       EGL_NONE};
	EGLint context_attr[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
	EGLConfig config;
	EGLint numConfigs;
	EGLint format;
	EGLint width;
	EGLint height;
	gl_buffers *glbuf = new gl_buffers;

	strm->display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
	ms_message("%s: %d strm->display: %p", __FILE__, __LINE__, strm->display);
	if (strm->display == EGL_NO_DISPLAY || !eglInitialize(strm->display, NULL, NULL)) {
		ms_message("ohos Unable to %s %d", "initialize OpenGL displa", eglGetError());
		deinit_opengl(strm);
		return;
	}

	if (!eglChooseConfig(strm->display, attr, &config, 1, &numConfigs) ||
	    (!eglGetConfigAttrib(strm->display, config, EGL_NATIVE_VISUAL_ID, &format))) {
		ms_message("ohos Unable to %s %d", "configure OpenGL display", eglGetError());
		deinit_opengl(strm);
		return;
	}

	strm->context = eglCreateContext(strm->display, config, EGL_NO_CONTEXT, context_attr);
	if (strm->context == EGL_NO_CONTEXT) {
		ms_message("ohos Unable to %s %d", "create OpenGL context", eglGetError());
		deinit_opengl(strm);
		return;
	}

	strm->surface = eglCreateWindowSurface(strm->display, config, (EGLNativeWindowType)(strm->window), 0);
	if (strm->surface == EGL_NO_SURFACE) {
		ms_message("ohos Unable to %s %d", "create window surface", eglGetError());
		deinit_opengl(strm);
		return;
	}

	if (!eglMakeCurrent(strm->display, strm->surface, strm->surface, strm->context)) {
		ms_message("ohos Unable to %s %d", "make OpenGL as current context", eglGetError());
		deinit_opengl(strm);
		return;
	}

	if (!eglQuerySurface(strm->display, strm->surface, EGL_WIDTH, &width) ||
	    !eglQuerySurface(strm->display, strm->surface, EGL_HEIGHT, &height)) {
		ms_message("ohos Unable to %s %d", "query surface", eglGetError());
		deinit_opengl(strm);
		return;
	}

	strm->gl_buf = glbuf;
	glDisable(GL_DEPTH_TEST);

	if (!(glbuf->direct = 1)) {
		glGenFramebuffers(1, &glbuf->frameBuf);
		glBindFramebuffer(GL_FRAMEBUFFER, glbuf->frameBuf);

		glGenRenderbuffers(1, &glbuf->rendBuf);
		glBindRenderbuffer(GL_RENDERBUFFER, glbuf->rendBuf);
	}

	glGenTextures(1, &glbuf->rendTex);

	/* Attributes */
	GLint attribLocation[NUM_ATTRIBUTES] = {ATTRIB_VERTEX, ATTRIB_TEXTUREPOSITON};
	GLchar *attribName[NUM_ATTRIBUTES] = {(char *)"position", (char *)"texCoord"};

	if (!glbuf->direct) {
		glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &glbuf->rendBufW);
		glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &glbuf->rendBufH);

		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, glbuf->rendBuf);
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
			ms_message("Unable to create frame buffer");
			return;
		}
	}

	create_program(vertSrc, fragSrc, NUM_ATTRIBUTES, (const GLchar **)&attribName[0], attribLocation,
	               &glbuf->directProg);

	if (!glbuf->directProg) {
		ms_message("Unable to create program");
		return;
	}

	ms_message("%s: %d", __FILE__, __LINE__);
	return;
}

static int ohos_dec_set_window(MSFilter *f, void *arg) {
	ms_message("%s: %d arg: %p", __FILE__, __LINE__, (OHNativeWindow *)(*(unsigned long *)arg));
	struct ohosgl_stream *d = (struct ohosgl_stream *)f->data;
	d->window = (OHNativeWindow *)(*(unsigned long *)arg);
	return 0;
}

static int ohos_display_set_zoom(MSFilter *f, void *arg) {
	ms_message("%s: %d arg: %p", __FILE__, __LINE__, arg);
	return 0;
}

static void ohos_display_init(MSFilter *f) {
	ohosgl_stream *op = new ohosgl_stream();
	f->data = op;
	ms_message("%s: %d op: %p", __FILE__, __LINE__, op);
}

static void ohos_display_uninit(MSFilter *f) {
	ms_message("%s: %d", __FILE__, __LINE__);
	deinit_opengl(f->data);
}

static void ohos_display_preprocess(BCTBX_UNUSED(MSFilter *f)) {
}

int pjmedia_vid_dev_opengl_draw(gl_buffers *glb, unsigned int width, unsigned int height, void *pixels) {
	static const GLfloat squareVertices[] = {
	    -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f,
	};
	GLfloat textureVertices[] = {0, 1, 1, 1, 0, 0, 1, 0};

	glBindTexture(GL_TEXTURE_2D, glb->rendTex);

	/* Set texture parameters */
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_BGRA, GL_UNSIGNED_BYTE,
	             (GLvoid *)pixels);

	glFlush();

	/* Do we render directly to the screen? */
	glBindFramebuffer(GL_FRAMEBUFFER, (glb->direct ? 0 : glb->frameBuf));
	/* Set the view port to the entire view */
	glViewport(0, 0, (glb->direct ? width : glb->rendBufW), (glb->direct ? height : glb->rendBufH));

	/* Draw the texture on the screen with OpenGL ES 2 */
	/* Use program */
	glUseProgram(glb->directProg);

	/* Update attribute values */
	glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices);
	glEnableVertexAttribArray(ATTRIB_VERTEX);
	glVertexAttribPointer(ATTRIB_TEXTUREPOSITON, 2, GL_FLOAT, 0, 0, textureVertices);
	glEnableVertexAttribArray(ATTRIB_TEXTUREPOSITON);

	/* Update uniform values if there are any */
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

	/* Present */
	if (!glb->direct) glBindRenderbuffer(GL_RENDERBUFFER, glb->rendBuf);

	return 0;
}

static void ohos_display_process(MSFilter *f) {
	ms_message("%s: %d", __FILE__, __LINE__);

	struct ohosgl_stream *stream = (struct ohosgl_stream *)f->data;
	if (stream != NULL && stream->gl_buf == NULL) {
		init11_opengl(stream);
	}

	mblk_t *m;
	MSPicture pic;
	ms_filter_lock(f);
	if ((m = ms_queue_peek_last(f->inputs[0])) != NULL) {
		if (ms_yuv_buf_init_from_mblk(&pic, m) == 0) {
			ms_message("%s: %d w: %d, h: %d", __FILE__, __LINE__, pic.w, pic.h);

			uint8_t *rgba_buf = (uint8_t *)malloc(pic.w * pic.h * 4); // or use aligned memory pool
			libyuv::I420ToARGB(pic.planes[0], pic.strides[0], pic.planes[1], pic.strides[1], pic.planes[2],
			                   pic.strides[2], rgba_buf, pic.w * 4, pic.w, pic.h);

			pjmedia_vid_dev_opengl_draw(stream->gl_buf, pic.w, pic.h, rgba_buf);
			free(rgba_buf);
			ms_message("%s: %d stream->display:%p, strm->surface: %p", __FILE__, __LINE__, stream->display,
			           stream->surface);
			if (!eglSwapBuffers(stream->display, stream->surface)) {
				if (eglGetError() == EGL_BAD_SURFACE && stream->err_rend > 0) {
					ms_message("%s: %d", __FILE__, __LINE__);
					stream->err_rend--;
					if (stream->err_rend == 0) {
						ms_message("%s: %d", __FILE__, __LINE__);
					}
				}
			}
		}
	}
	ms_filter_unlock(f);

	ms_queue_flush(f->inputs[0]);
	if (f->inputs[1] != NULL) ms_queue_flush(f->inputs[1]);
}

static MSFilterMethod methods[] = {{MS_VIDEO_DISPLAY_SET_NATIVE_WINDOW_ID, ohos_dec_set_window},
                                   {MS_VIDEO_DISPLAY_ZOOM, ohos_display_set_zoom},
                                   {0, NULL}};

MSFilterDesc ms_ohos_opengl_display_desc = {.id = MS_OHOS_OPENGL_DISPLAY_ID,
                                            .name = "MSOHOSOpenGLDisplay",
                                            .text = "OpenGL-ES2 video display filter for OHOS.",
                                            .category = MS_FILTER_OTHER,
                                            .ninputs = 2,  /*number of inputs*/
                                            .noutputs = 0, /*number of outputs*/
                                            .init = ohos_display_init,
                                            .preprocess = ohos_display_preprocess,
                                            .process = ohos_display_process,
                                            .uninit = ohos_display_uninit,
                                            .methods = methods};

void libmsohosopengldisplay_init(MSFactory *factory) {
	ms_factory_register_filter(factory, &ms_ohos_opengl_display_desc);
	ms_message("MSAndroidDisplay (OpenGL ES2) registered (id=%d).", MS_OHOS_OPENGL_DISPLAY_ID);
}

#ifdef __cplusplus
}
#endif